/*
 * Copyright (c) 2005, the JUNG Project and the Regents of the University of
 * California All rights reserved.
 *
 * This software is open-source under the BSD license; see either "license.txt"
 * or http://jung.sourceforge.net/license.txt for a description.
 *
 * Created on Aug 23, 2005
 */
package visualization;

import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;

import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.renderers.EdgeArrowRenderingSupport;

public class OligoEdgeArrowRenderingSupport<V, E> implements
		EdgeArrowRenderingSupport<V, E> {

	/*
	 * (non-Javadoc)
	 * 
	 * @see edu.uci.ics.jung.visualization.renderers.EdgeArrowRenderingSupport#
	 * getArrowTransform(edu.uci.ics.jung.visualization.RenderContext,
	 * java.awt.geom.GeneralPath, java.awt.Shape)
	 */
	public AffineTransform getArrowTransform(RenderContext<V, E> rc,
			Shape edgeShape, Shape vertexShape) {
		GeneralPath path = new GeneralPath(edgeShape);
		float[] seg = new float[6];
		Point2D p1 = null;
		Point2D p2 = null;
		AffineTransform at = new AffineTransform();
		AffineTransform firstAt = new AffineTransform();
		// when the PathIterator is done, switch to the line-subdivide
		// method to get the arrowhead closer.
		boolean out = false;
		boolean found = false;
		boolean firstFound = false;
		for (PathIterator i = path.getPathIterator(null, 1); !i.isDone(); i
				.next()) {
			int ret = i.currentSegment(seg);
			if (ret == PathIterator.SEG_MOVETO) {
				p2 = new Point2D.Float(seg[0], seg[1]);
			} else if (ret == PathIterator.SEG_LINETO) {
				p1 = p2;
				p2 = new Point2D.Float(seg[0], seg[1]);
				if (vertexShape.contains(p2)) {
					if (out) {
						at = getArrowTransform(rc, new Line2D.Float(p1, p2),
								vertexShape);
						found = true;
						break;
					} else if (!firstFound) {
						firstAt = getArrowTransform(rc,
								new Line2D.Float(p1, p2), vertexShape);
						firstFound = true;
					}
				} else {
					out = true;
				}
			}
		}
		if (found) {
			return at;
		} else {
			return firstAt;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see edu.uci.ics.jung.visualization.renderers.EdgeArrowRenderingSupport#
	 * getReverseArrowTransform(edu.uci.ics.jung.visualization.RenderContext,
	 * java.awt.geom.GeneralPath, java.awt.Shape)
	 */
	public AffineTransform getReverseArrowTransform(RenderContext<V, E> rc,
			Shape edgeShape, Shape vertexShape) {
		return getReverseArrowTransform(rc, edgeShape, vertexShape, true);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see edu.uci.ics.jung.visualization.renderers.EdgeArrowRenderingSupport#
	 * getReverseArrowTransform(edu.uci.ics.jung.visualization.RenderContext,
	 * java.awt.geom.GeneralPath, java.awt.Shape, boolean)
	 */
	public AffineTransform getReverseArrowTransform(RenderContext<V, E> rc,
			Shape edgeShape, Shape vertexShape, boolean passedGo) {
		GeneralPath path = new GeneralPath(edgeShape);
		float[] seg = new float[6];
		Point2D p1 = null;
		Point2D p2 = null;

		AffineTransform at = new AffineTransform();
		for (PathIterator i = path.getPathIterator(null, 1); !i.isDone(); i
				.next()) {
			int ret = i.currentSegment(seg);
			if (ret == PathIterator.SEG_MOVETO) {
				p2 = new Point2D.Float(seg[0], seg[1]);
			} else if (ret == PathIterator.SEG_LINETO) {
				p1 = p2;
				p2 = new Point2D.Float(seg[0], seg[1]);
				if (passedGo == false && vertexShape.contains(p2)) {
					passedGo = true;
				} else if (passedGo == true
						&& vertexShape.contains(p2) == false) {
					at = getReverseArrowTransform(rc, new Line2D.Float(p1, p2),
							vertexShape);
					break;
				}
			}
		}
		return at;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see edu.uci.ics.jung.visualization.renderers.EdgeArrowRenderingSupport#
	 * getArrowTransform(edu.uci.ics.jung.visualization.RenderContext,
	 * java.awt.geom.Line2D, java.awt.Shape)
	 */
	public AffineTransform getArrowTransform(RenderContext<V, E> rc,
			Line2D edgeShape, Shape vertexShape) {
		float dx = (float) (edgeShape.getX1() - edgeShape.getX2());
		float dy = (float) (edgeShape.getY1() - edgeShape.getY2());
		// iterate over the line until the edge shape will place the
		// arrowhead closer than 'arrowGap' to the vertex shape boundary
		while ((dx * dx + dy * dy) > rc.getArrowPlacementTolerance()) {
			try {
				edgeShape = getLastOutsideSegment(edgeShape, vertexShape);
			} catch (IllegalArgumentException e) {
				System.err.println(e.toString());
				return null;
			}
			dx = (float) (edgeShape.getX1() - edgeShape.getX2());
			dy = (float) (edgeShape.getY1() - edgeShape.getY2());
		}
		double atheta = Math.atan2(dx, dy) + Math.PI / 2;
		AffineTransform at = AffineTransform.getTranslateInstance(
				edgeShape.getX1(), edgeShape.getY1());
		at.rotate(-atheta);
		return at;
	}

	/**
	 * This is used for the reverse-arrow of a non-directed edge get a transform
	 * to place the arrow shape on the passed edge at the point where it
	 * intersects the passed shape
	 * 
	 * @param edgeShape
	 * @param vertexShape
	 * @return
	 */
	protected AffineTransform getReverseArrowTransform(RenderContext<V, E> rc,
			Line2D edgeShape, Shape vertexShape) {
		float dx = (float) (edgeShape.getX1() - edgeShape.getX2());
		float dy = (float) (edgeShape.getY1() - edgeShape.getY2());
		// iterate over the line until the edge shape will place the
		// arrowhead closer than 'arrowGap' to the vertex shape boundary
		while ((dx * dx + dy * dy) > rc.getArrowPlacementTolerance()) {
			try {
				edgeShape = getFirstOutsideSegment(edgeShape, vertexShape);
			} catch (IllegalArgumentException e) {
				System.err.println(e.toString());
				return null;
			}
			dx = (float) (edgeShape.getX1() - edgeShape.getX2());
			dy = (float) (edgeShape.getY1() - edgeShape.getY2());
		}
		// calculate the angle for the arrowhead
		double atheta = Math.atan2(dx, dy) - Math.PI / 2;
		AffineTransform at = AffineTransform.getTranslateInstance(
				edgeShape.getX1(), edgeShape.getY1());
		at.rotate(-atheta);
		return at;
	}

	/**
	 * Passed Line's point2 must be inside the passed shape or an
	 * IllegalArgumentException is thrown
	 * 
	 * @param line
	 *            line to subdivide
	 * @param shape
	 *            shape to compare with line
	 * @return a line that intersects the shape boundary
	 * @throws IllegalArgumentException
	 *             if the passed line's point1 is not inside the shape
	 */
	protected Line2D getLastOutsideSegment(Line2D line, Shape shape) {
		if (shape.contains(line.getP2()) == false) {
			String errorString = "line end point: " + line.getP2()
					+ " is not contained in shape: " + shape.getBounds2D();
			throw new IllegalArgumentException(errorString);
			// return null;
		}
		Line2D left = new Line2D.Double();
		Line2D right = new Line2D.Double();
		// subdivide the line until its left segment intersects
		// the shape boundary
		do {
			subdivide(line, left, right);
			line = right;
		} while (shape.contains(line.getP1()) == false);
		// now that right is completely inside shape,
		// return left, which must be partially outside
		return left;
	}

	/**
	 * Passed Line's point1 must be inside the passed shape or an
	 * IllegalArgumentException is thrown
	 * 
	 * @param line
	 *            line to subdivide
	 * @param shape
	 *            shape to compare with line
	 * @return a line that intersects the shape boundary
	 * @throws IllegalArgumentException
	 *             if the passed line's point1 is not inside the shape
	 */
	protected Line2D getFirstOutsideSegment(Line2D line, Shape shape) {

		if (shape.contains(line.getP1()) == false) {
			String errorString = "line start point: " + line.getP1()
					+ " is not contained in shape: " + shape.getBounds2D();
			throw new IllegalArgumentException(errorString);
		}
		Line2D left = new Line2D.Float();
		Line2D right = new Line2D.Float();
		// subdivide the line until its right side intersects the
		// shape boundary
		do {
			subdivide(line, left, right);
			line = left;
		} while (shape.contains(line.getP2()) == false);
		// now that left is completely inside shape,
		// return right, which must be partially outside
		return right;
	}

	/**
	 * divide a Line2D into 2 new Line2Ds that are returned in the passed left
	 * and right instances, if non-null
	 * 
	 * @param src
	 *            the line to divide
	 * @param left
	 *            the left side, or null
	 * @param right
	 *            the right side, or null
	 */
	protected void subdivide(Line2D src, Line2D left, Line2D right) {
		double x1 = src.getX1();
		double y1 = src.getY1();
		double x2 = src.getX2();
		double y2 = src.getY2();

		double mx = x1 + (x2 - x1) / 2.0;
		double my = y1 + (y2 - y1) / 2.0;
		if (left != null) {
			left.setLine(x1, y1, mx, my);
		}
		if (right != null) {
			right.setLine(mx, my, x2, y2);
		}
	}

}
